﻿using System;
using System.Collections.Generic;
using System.Windows;
using System.Xml.Linq;
using GameEngine.Libs;
using GameEngine.Model.Data;
using SLPerlinNoise;
using GameEngine.View.Components;
using System.Windows.Input;
using MapEngine;

namespace GameEngine.Model
{
    /// <summary>
    /// </summary>
    public class MapDataProxy<ItemVOId> : DataProxy, IMapDataProxy<ItemVOId>
        where ItemVOId : IComparable<ItemVOId>
    {
        public string CAMERAPOSITIONUPDATED_NOTIFICATION { 
            get; 
            protected set; 
        }
        public string TYPEUPDATED_NOTIFICATION
        {
            get;
            protected set;
        }
        public string SCALEUPDATED_NOTIFICATION
        {
            get;
            protected set;
        }
        public string TILESUPDATED_NOTIFICATION
        {
            get;
            protected set;
        }
        public string TILEHEIGHTUPDATED_NOTIFICATION
        {
            get;
            protected set;
        }
        public string CURSORUPDATED_NOTIFICATION
        {
            get;
            protected set;
        }

        public IMapVO<ItemVOId> VO
        {
            get { return Data as IMapVO<ItemVOId>; }
        }
        
        //protected IWorldDataProxy<ItemVOId> WorldProxy
        //{
        //    get;
        //    set;
        //}
        
        public Point TileSize { get; set; }
        
        public float MaxHeight { get; set; }

        public string Name {
            get { return ProxyName; }
        }

        public Point CameraPosition
        {
            get
            {
                return VO.Camera.Position;
            }
            set
            {
                if (VO.Size.X == -1 || value.X <= VO.Size.X && value.X >= 0)
                    VO.Camera.Position = new Point(value.X, VO.Camera.Position.Y);
                if (VO.Size.Y == -1 || value.Y <= VO.Size.Y && value.Y >= 0)
                    VO.Camera.Position = new Point(VO.Camera.Position.X, value.Y);
                SendNotification(CAMERAPOSITIONUPDATED_NOTIFICATION, this);
            }
        }

        public Point Size
        {
            get { return VO.Size; }
        }

        public float Scale
        {
            get { return VO.Camera.Scale; }
            set
            {
                VO.Camera.Scale = value;
                SendNotification(SCALEUPDATED_NOTIFICATION, this);
            }
        }

        public TileType TileType { get { return VO.TileType; } set { 
            VO.TileType = value;
            SendNotification(TYPEUPDATED_NOTIFICATION, this);
        } }

        public MapType MapType
        {
            get { return VO.MapType; }
            set
            {
                VO.MapType = value;
                SendNotification(TYPEUPDATED_NOTIFICATION, this);
            }
        }

        PathFinder pathFinder { get; set; }

        Cursor cursor = Cursors.Arrow;
        public Cursor Cursor {
            get { return cursor; } 
            set {
                cursor = value;
                SendNotification(CURSORUPDATED_NOTIFICATION, cursor);
            } 
        }

        public List<List<ITileVO>> Tiles {
            get { return VO.Tiles; }
            set { VO.Tiles = value;
            SendNotification(TILESUPDATED_NOTIFICATION, this);
            }
        }

        public List<IViewVO<ItemVOId>> SimpleViews { get; set; }

        public MapDataProxy(IMapVO<ItemVOId> map, Point tileSize, float maxHeight)
            : base(IMapDataProxyConstants<ItemVOId>.CreateDataProxyName(map), map)
        {
            //WorldProxy = worldProxy;
            TileSize = tileSize;
            MaxHeight = maxHeight;
            CAMERAPOSITIONUPDATED_NOTIFICATION = "MapDataProxy_camaraPositionUpdated_" + map.Name;
            TYPEUPDATED_NOTIFICATION = "MapDataProxy_typeUpdated_" + map.Name;
            SCALEUPDATED_NOTIFICATION = "MapDataProxy_scaleUpdated_" + map.Name;
            TILESUPDATED_NOTIFICATION = "MapDataProxy_TilesUpdated_" + map.Name;
            TILEHEIGHTUPDATED_NOTIFICATION = "MapDataProxy_TileHeightUpdated_" + map.Name;
            CURSORUPDATED_NOTIFICATION = "MapDataProxy_CursorUpdated_" + map.Name;

            SimpleViews = new List<IViewVO<ItemVOId>>();
            foreach(var viewId in map.SimpleViews)
                SimpleViews.Add(ModelFacade<ItemVOId>.Instance.Game.Views[viewId]);

            pathFinder = new AStarPathFinder(this, 10, false);
        }

        public void CreateTiles(Point size) {
            VO.CreateTiles(size);
            SendNotification(TILESUPDATED_NOTIFICATION, this);
        }

        public void GenerateHeightsFromPerlin(Point origin, float frequency, float amplitude, float persistence, int octaves, int seed)
        {
            IGameDataProxy<ItemVOId> gameDP = ModelFacade<ItemVOId>.Instance.Game;
            PerlinNoise3D.GenerateHeightsFromPerlin(VO.Tiles, origin, frequency, amplitude, persistence, octaves, seed, gameDP.MinHeight, gameDP.MaxHeight);
            SendNotification(TILESUPDATED_NOTIFICATION, this);
        }

        /**
         * Find a path from the starting location provided (sx,sy) to the target
         * location (tx,ty) avoiding blockages and attempting to honour costs 
         * provided by the tile map.
         * 
         * @param mover The entity that will be moving along the path. This provides
         * a place to pass context information about the game entity doing the moving, e.g.
         * can it fly? can it swim etc.
         * 
         * @param sx The x coordinate of the start location
         * @param sy The y coordinate of the start location
         * @param tx The x coordinate of the target location
         * @param ty Teh y coordinate of the target location
         * @return The path found from start to end, or null if no path can be found.
         */
        public Path findPath(Mover mover, int sx, int sy, int tx, int ty) {
            return pathFinder.findPath(mover, sx, sy, tx, ty);
        }

        public Point GetNearestEmptyPosition(IThingDataProxy<ItemVOId> mover, IThingDataProxy<ItemVOId> destiny) {
            //TODO better
            return new Point(destiny.Position.X,destiny.Position.Y-1);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="position">tile coords</param>
        /// <returns></returns>
        public ITileVO GetTile(Point position)
        {
            //            switch (Map.type) { 
            //                case MapVO.Type.Normal:
            if (position.X < VO.Tiles.Count && position.X >= 0 &&
                position.Y < VO.Tiles[(int)position.X].Count && position.Y >= 0
                )
                return VO.Tiles[(int)position.X][(int)position.Y];
            /*                    break;
                            case MapVO.Type.Perlin:
                                TileVO tile = new TileVO() { Height = GetPerlinHeight(position) };
                                //color??
                                return tile;
                        }*/
            return null;
        }

        public XDocument SaveToXML()
        {
            XDocument doc = new XDocument();
            XElement mapNode = new XElement("map");
            XElement size = new XElement("size");
            size.SetAttributeValue("x", VO.Size.X);
            size.SetAttributeValue("y", VO.Size.Y);
            mapNode.Add(size);
            //todo Camera
            XElement tilesNode = new XElement("tiles");
            foreach (List<ITileVO> tiles in VO.Tiles)
            {
                XElement rowNode = new XElement("row");
                foreach (ITileVO tile in tiles)
                {
                    XElement tileNode = new XElement("tile");
                    tileNode.SetAttributeValue("height", tile.Height.ToString());
                    XElement position = new XElement("position");
                    position.SetAttributeValue("x", tile.Position.X);
                    position.SetAttributeValue("y", tile.Position.Y);
                    tileNode.Add(position);
                    rowNode.Add(tileNode);
                }
                tilesNode.Add(rowNode);
            }
            mapNode.Add(tilesNode);
            doc.Add(mapNode);
            return doc;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="position">tile coords</param>
        /// <returns></returns>
        public float? GetTileHeight(Point position)
        {
            ITileVO tile = GetTile(position);
            if (tile != null)
                return tile.Height;
            else
                return null;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="position">free map position</param>
        /// <returns></returns>
        /*public KeyValuePair<float,Point>? GetHeight(Point position)
        {
            Point tilePosition = GetTileFromPosition(position);
            TileVO tile = GetTile(tilePosition);
            if (tile == null)
                return null;
            float height;
            if (tilePosition.X > position.X)
            {
                if (tilePosition.Y > position.Y)
                {
                    height = (tile.Height 
                        + GetTile(new Point(tilePosition.X + 1, tilePosition.Y + 1)).Height
                        + GetTile(new Point(tilePosition.X , tilePosition.Y + 1)).Height
                        + GetTile(new Point(tilePosition.X + 1, tilePosition.Y )).Height
                        ) / 4;
                }
                else
                {
                    height = (tile.Height
                        + GetTile(new Point(tilePosition.X + 1, tilePosition.Y - 1)).Height
                        + GetTile(new Point(tilePosition.X, tilePosition.Y - 1)).Height
                        + GetTile(new Point(tilePosition.X + 1, tilePosition.Y)).Height
                        ) / 4;
                }
            }
            else {
                if (tilePosition.Y > position.Y)
                {
                    height = (tile.Height
                        + GetTile(new Point(tilePosition.X - 1, tilePosition.Y + 1)).Height
                        + GetTile(new Point(tilePosition.X, tilePosition.Y + 1)).Height
                        + GetTile(new Point(tilePosition.X - 1, tilePosition.Y)).Height
                        ) / 4;
                }
                else
                {
                    height = (tile.Height
                        + GetTile(new Point(tilePosition.X - 1, tilePosition.Y - 1)).Height
                        + GetTile(new Point(tilePosition.X, tilePosition.Y - 1)).Height
                        + GetTile(new Point(tilePosition.X - 1, tilePosition.Y)).Height
                        ) / 4;
                }
            }
            float hip = Math.Sqrt(
            height = tile.Height - height
            
            return new KeyValuePair<float,Point>(height,new Point);
                
        }*/

        public void SetTileHeight(Point position, float height)
        {
            ITileVO tile = GetTile(position);
            if (tile != null)
            {
                tile.Height = height;
                SendNotification(TILEHEIGHTUPDATED_NOTIFICATION, tile);
            }
        }

        /// <summary>
        /// Returns the tile coords from a 2d map position (not the transformed screen position)
        /// </summary>
        /// <param name="position">free map position</param>
        /// <returns></returns>
        public Point GetTileFromPosition(Point position)
        {
            //todo switch map type
            return new Point(Math.Floor(position.X / TileSize.X), Math.Floor(position.Y / TileSize.Y));
        }

        /*public Point GetTileFromScreenPosition(Point position){
            //todo swtich map type
            
        }*/

        /*
                List<List<TileVO>> Tiles {
                    get { return Map.Tiles; }
                    set {
                        Map.type = MapVO.Type.Normal;
                        Map.Tiles = value;
                        Map.Size.X = Map.Tiles.Count;
                        if (Map.Size.X > 0)
                            Map.Size.Y = Map.Tiles[0].Count;
                        else
                            Map.Size.Y = 0;
                    }
                }*/

        /*
        List<List<float>> getMap(Point position, Point size) {
            List<List<float>> map = new List<List<float>>();
            for (double row = position.Y - size.Y / 2; row < position.Y + size.Y / 2; row++)
            {
                map.Add(getRow(new Point(position.X, row), size));
            }
            return map;
        }

        List<float> getRow(Point position, Point size)
        {
            List<float> row = new List<float>();
            for (double column = position.X - size.X / 2; column < position.X + size.X / 2; column++)
            {
                //row.Add(MaxMapHeight * (Generator.Compute((float)column, (float)position.Y, 0)));
                row.Add(GetPerlinHeight(position));
            }
            return row;
        }

        List<float> getColumn(Point position, Point size)
        {
            //return getMap(position, new Point(1, 1))[0];
            return getMap(position, size)[0];
        }

        List<List<float>> getMapOld(Point position, Point size)
        {
             new HeightMap(
                        100,       //maxMapWidth
                        100,       //maxMapHeight
                        1f,    //frequency
                        1.0f,       //amplitude
                        0.5f,       //persistance
                        16,         //octaves     
                        1).getMap(  //seed
                            new Point(0, 0),    //position
                            new Point(1.7 * MapContainer.Width / (TileSize.X), 1.7 * MapContainer.Height / (TileSize.Y)), //map size
                            new Point(2,2),
                            10              //maxHeight
                        )
            return null;
        }

        
        public List<TileVO> Tiles {
            get {
                DataProxy.Instance.RetrieveData(new XElement("TileMap",TileMap));
                return null;
            }
        }*/

        public void Tick()
        {
            //throw new NotImplementedException();
        }

        //public void LoadFromXML(XDocument doc)
        //{
        //    List<List<ITileVO>> tiles = new List<List<ITileVO>>();
        //    foreach (XElement row in doc.Elements("tiles").Nodes())
        //    {
        //        List<ITileVO> tileRow = new List<ITileVO>();
        //        foreach (XElement tile in row.Nodes())
        //        {
        //            ITileVO tileVO = ApplicationFacade.Instance.CreateTileVO();
        //            tileVO.Height = float.Parse(tile.Attribute("height").Value);
        //            foreach (XElement node in tile.Nodes())
        //                switch (node.Name.ToString())
        //                {
        //                    case "position":
        //                        tileVO.Position = new Point(
        //                            int.Parse(node.Attribute("x").Value),
        //                            int.Parse(node.Attribute("y").Value));
        //                        break;
        //                }
        //            tileRow.Add(tileVO);
        //        }
        //        tiles.Add(tileRow);
        //    }
        //    VO.Tiles = tiles;
        //    SendNotification(TILESUPDATED_NOTIFICATION, this);
        //}

        public int DistanceInTiles(Point position, Point position2) {
            double value = Math.Sqrt(
                Math.Pow(Math.Floor(position2.X) - Math.Floor(position.X), 2) +
                Math.Pow(Math.Floor(position2.Y) - Math.Floor(position.Y), 2));
            return (int)value;
        }

        //public override string Serialize()
        //{
        //    return Serializer.Serialize(this);
        //}

        #region TileBasedMap interface
        /**
         * Get the width of the tile map. The slightly odd name is used
         * to distiguish this method from commonly used names in game maps.
         * 
         * @return The number of tiles across the map
         */
        public int getWidthInTiles() {
            return (int)VO.Size.X;
        }

        /**
         * Get the height of the tile map. The slightly odd name is used
         * to distiguish this method from commonly used names in game maps.
         * 
         * @return The number of tiles down the map
         */
        public int getHeightInTiles()
        {
            return (int)VO.Size.Y;
        }

        /**
         * Notification that the path finder visited a given tile. This is 
         * used for debugging new heuristics.
         * 
         * @param x The x coordinate of the tile that was visited
         * @param y The y coordinate of the tile that was visited
         */
        public void pathFinderVisited(int x, int y) { 
        }

        /**
         * Check if the given location is blocked, i.e. blocks movement of 
         * the supplied mover.
         * 
         * @param mover The mover that is potentially moving through the specified
         * tile.
         * @param x The x coordinate of the tile to check
         * @param y The y coordinate of the tile to check
         * @return True if the location is blocked
         */
        public bool blocked(Mover mover, int x, int y) {
            //bool occupied = world.ItemsMapPosition.ContainsKey(new Point(x, y));
            //if (world.ItemsMapPosition[newMapDestination].Value.CompareTo(item.Id.Value) != 0)
            if (x < 0 || y < 0)
                return true;
            if (VO.Tiles.Count < x)
                return true;
            if (VO.Tiles[x].Count < y)
                return true;
            return VO.Tiles[x][y].Occupied || VO.Tiles[x][y].Height<0;
        }

        /**
         * Get the cost of moving through the given tile. This can be used to 
         * make certain areas more desirable. A simple and valid implementation
         * of this method would be to return 1 in all cases.
         * 
         * @param mover The mover that is trying to move across the tile
         * @param sx The x coordinate of the tile we're moving from
         * @param sy The y coordinate of the tile we're moving from
         * @param tx The x coordinate of the tile we're moving to
         * @param ty The y coordinate of the tile we're moving to
         * @return The relative cost of moving across the given tile
         */
        public float getCost(Mover mover, int sx, int sy, int tx, int ty) {
            return 1;
        }
        #endregion
    }
}
